iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
Build on AWS

30 天將工作室 SaaS 產品部署起來系列 第 26

Day 26: 30天部署SaaS產品到AWS-AWS WAF 與 Shield 安全防護

  • 分享至 

  • xImage
  •  

前情提要

經過 Day 25 的 Cognito 用戶池建置,我們已經有了完整的身份認證服務。今天我們要為 Kyo System 部署 AWS WAF (Web Application Firewall)AWS Shield 安全防護。在雲端環境中,應用層攻擊(如 DDoS、SQL Injection、XSS)是主要威脅,我們需要在 CloudFront 和 ALB 層面建立多層防護,確保系統的可用性與安全性。

AWS WAF vs Shield 架構概覽

/**
 * AWS 安全防護架構
 *
 * ┌──────────────────────────────────────────────┐
 * │        AWS Security Layer Architecture      │
 * └──────────────────────────────────────────────┘
 *
 * Layer 1: AWS Shield (DDoS Protection)
 * ┌─────────────────────────────────────┐
 * │  ┌──────────┐      ┌──────────┐    │
 * │  │ Shield   │      │ Shield   │    │
 * │  │ Standard │      │ Advanced │    │
 * │  └──────────┘      └──────────┘    │
 * │    • 自動啟用         • $3000/月   │
 * │    • L3/L4 DDoS      • 進階保護   │
 * │    • 免費             • 成本保護   │
 * └─────────────────────────────────────┘
 *                ↓
 * Layer 2: AWS WAF (Application Firewall)
 * ┌─────────────────────────────────────┐
 * │  CloudFront / ALB / API Gateway     │
 * │                                      │
 * │  ✅ SQL Injection 防護              │
 * │  ✅ XSS (Cross-Site Scripting)     │
 * │  ✅ Rate-based Rules                │
 * │  ✅ Geo Blocking                    │
 * │  ✅ IP Sets (Whitelist/Blacklist)  │
 * │  ✅ Bot Control                     │
 * │  ✅ Custom Rules                    │
 * └─────────────────────────────────────┘
 *                ↓
 * Layer 3: Application Security
 * ┌─────────────────────────────────────┐
 * │  • API Rate Limiting (自建)         │
 * │  • JWT Validation                   │
 * │  • Input Validation                 │
 * │  • RBAC Authorization               │
 * └─────────────────────────────────────┘
 *
 * 定價 (us-east-1):
 * WAF:
 *   - Web ACL: $5.00/month
 *   - Rules: $1.00/month per rule
 *   - Requests: $0.60 per 1M requests
 *   - Bot Control: $10.00/month + $1.00 per 1M requests
 *
 * Shield Advanced:
 *   - $3,000/month per organization
 *   - DDoS Response Team (DRT) 支援
 *   - 成本保護(DDoS 期間流量費用減免)
 *   - 進階指標與報告
 *
 * 建議:
 * - 一般 SaaS: WAF + Shield Standard
 * - 高可用需求: WAF + Shield Advanced
 * - 成本敏感: WAF only
 */

CDK 建立 WAF Web ACL

// infrastructure/lib/waf-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as cloudwatch_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
import { Construct } from 'constructs';

export interface WAFStackProps extends cdk.StackProps {
  scope: 'CLOUDFRONT' | 'REGIONAL';
  resourceArn?: string; // ALB/API Gateway ARN (REGIONAL 需要)
}

export class WAFStack extends cdk.Stack {
  public readonly webACL: wafv2.CfnWebACL;

  constructor(scope: Construct, id: string, props: WAFStackProps) {
    super(scope, id, props);

    /**
     * 建立 IP Sets
     */

    // 允許清單(可信任的 IP)
    const allowedIPSet = new wafv2.CfnIPSet(this, 'AllowedIPSet', {
      name: 'kyo-allowed-ips',
      scope: props.scope,
      ipAddressVersion: 'IPV4',
      addresses: [
        // 範例:辦公室 IP
        '203.0.113.0/24',
        // 範例:CI/CD IP
        '198.51.100.0/24',
      ],
      description: 'Trusted IP addresses (office, CI/CD, etc.)',
    });

    // 封鎖清單(已知惡意 IP)
    const blockedIPSet = new wafv2.CfnIPSet(this, 'BlockedIPSet', {
      name: 'kyo-blocked-ips',
      scope: props.scope,
      ipAddressVersion: 'IPV4',
      addresses: [
        // 動態更新(透過 Lambda 或手動)
      ],
      description: 'Blocked IP addresses',
    });

    /**
     * 建立 Web ACL
     */
    this.webACL = new wafv2.CfnWebACL(this, 'KyoWebACL', {
      name: 'kyo-web-acl',
      scope: props.scope,
      defaultAction: { allow: {} }, // 預設允許

      // 規則列表(依優先順序執行)
      rules: [
        // Rule 1: 封鎖清單 IP
        {
          name: 'BlockKnownBadIPs',
          priority: 0,
          statement: {
            ipSetReferenceStatement: {
              arn: blockedIPSet.attrArn,
            },
          },
          action: { block: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'BlockKnownBadIPs',
          },
        },

        // Rule 2: 允許清單 IP(跳過後續檢查)
        {
          name: 'AllowTrustedIPs',
          priority: 1,
          statement: {
            ipSetReferenceStatement: {
              arn: allowedIPSet.attrArn,
            },
          },
          action: { allow: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'AllowTrustedIPs',
          },
        },

        // Rule 3: AWS Managed Rules - Core Rule Set
        {
          name: 'AWSManagedRulesCommonRuleSet',
          priority: 2,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesCommonRuleSet',
              excludedRules: [
                // 排除誤判規則(依需求調整)
                // { name: 'SizeRestrictions_BODY' },
              ],
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'AWSManagedRulesCommonRuleSet',
          },
        },

        // Rule 4: SQL Injection 防護
        {
          name: 'AWSManagedRulesSQLiRuleSet',
          priority: 3,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesSQLiRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'AWSManagedRulesSQLiRuleSet',
          },
        },

        // Rule 5: Known Bad Inputs 防護
        {
          name: 'AWSManagedRulesKnownBadInputsRuleSet',
          priority: 4,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesKnownBadInputsRuleSet',
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'AWSManagedRulesKnownBadInputsRuleSet',
          },
        },

        // Rule 6: 地理封鎖(範例:只允許台灣、日本、美國)
        {
          name: 'GeoBlocking',
          priority: 5,
          statement: {
            notStatement: {
              statement: {
                geoMatchStatement: {
                  countryCodes: ['TW', 'JP', 'US', 'SG'],
                },
              },
            },
          },
          action: { block: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'GeoBlocking',
          },
        },

        // Rule 7: Rate-based Rule (API 速率限制)
        {
          name: 'RateLimitAPI',
          priority: 6,
          statement: {
            rateBasedStatement: {
              limit: 2000, // 5 分鐘內 2000 requests
              aggregateKeyType: 'IP',
              scopeDownStatement: {
                byteMatchStatement: {
                  fieldToMatch: { uriPath: {} },
                  positionalConstraint: 'STARTS_WITH',
                  searchString: '/api/',
                  textTransformations: [
                    { priority: 0, type: 'LOWERCASE' },
                  ],
                },
              },
            },
          },
          action: {
            block: {
              customResponse: {
                responseCode: 429,
                customResponseBodyKey: 'rate-limit-exceeded',
              },
            },
          },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'RateLimitAPI',
          },
        },

        // Rule 8: 登入端點特殊保護
        {
          name: 'LoginEndpointProtection',
          priority: 7,
          statement: {
            rateBasedStatement: {
              limit: 100, // 5 分鐘內 100 requests
              aggregateKeyType: 'IP',
              scopeDownStatement: {
                andStatement: {
                  statements: [
                    {
                      byteMatchStatement: {
                        fieldToMatch: { uriPath: {} },
                        positionalConstraint: 'EXACTLY',
                        searchString: '/api/auth/login',
                        textTransformations: [
                          { priority: 0, type: 'LOWERCASE' },
                        ],
                      },
                    },
                    {
                      byteMatchStatement: {
                        fieldToMatch: { method: {} },
                        positionalConstraint: 'EXACTLY',
                        searchString: 'POST',
                        textTransformations: [
                          { priority: 0, type: 'NONE' },
                        ],
                      },
                    },
                  ],
                },
              },
            },
          },
          action: { block: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'LoginEndpointProtection',
          },
        },

        // Rule 9: 自訂規則 - 封鎖特定 User-Agent
        {
          name: 'BlockBadBots',
          priority: 8,
          statement: {
            orStatement: {
              statements: [
                {
                  byteMatchStatement: {
                    fieldToMatch: {
                      singleHeader: { name: 'user-agent' },
                    },
                    positionalConstraint: 'CONTAINS',
                    searchString: 'badbot',
                    textTransformations: [
                      { priority: 0, type: 'LOWERCASE' },
                    ],
                  },
                },
                {
                  byteMatchStatement: {
                    fieldToMatch: {
                      singleHeader: { name: 'user-agent' },
                    },
                    positionalConstraint: 'CONTAINS',
                    searchString: 'scrapy',
                    textTransformations: [
                      { priority: 0, type: 'LOWERCASE' },
                    ],
                  },
                },
              ],
            },
          },
          action: { block: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'BlockBadBots',
          },
        },
      ],

      // 自訂回應內容
      customResponseBodies: {
        'rate-limit-exceeded': {
          contentType: 'APPLICATION_JSON',
          content: JSON.stringify({
            error: 'Too Many Requests',
            message: 'Rate limit exceeded. Please try again later.',
            code: 'RATE_LIMIT_EXCEEDED',
          }),
        },
      },

      // 可見性設定
      visibilityConfig: {
        sampledRequestsEnabled: true,
        cloudWatchMetricsEnabled: true,
        metricName: 'KyoWebACL',
      },
    });

    /**
     * 關聯到資源(REGIONAL 需要)
     */
    if (props.scope === 'REGIONAL' && props.resourceArn) {
      new wafv2.CfnWebACLAssociation(this, 'WebACLAssociation', {
        webAclArn: this.webACL.attrArn,
        resourceArn: props.resourceArn,
      });
    }

    /**
     * CloudWatch Alarms
     */
    const topic = new sns.Topic(this, 'WAFAlertTopic', {
      displayName: 'WAF Security Alerts',
    });

    // 告警 1: 封鎖請求過多
    const blockedRequestsAlarm = new cloudwatch.Alarm(this, 'BlockedRequestsAlarm', {
      alarmName: 'kyo-waf-high-blocked-requests',
      metric: new cloudwatch.Metric({
        namespace: 'AWS/WAFV2',
        metricName: 'BlockedRequests',
        dimensionsMap: {
          WebACL: this.webACL.name!,
          Rule: 'ALL',
          Region: this.region,
        },
        statistic: 'Sum',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 1000, // 5 分鐘內超過 1000 個封鎖請求
      evaluationPeriods: 1,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    blockedRequestsAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(topic));

    // 告警 2: Rate Limit 頻繁觸發
    const rateLimitAlarm = new cloudwatch.Alarm(this, 'RateLimitAlarm', {
      alarmName: 'kyo-waf-rate-limit-triggered',
      metric: new cloudwatch.Metric({
        namespace: 'AWS/WAFV2',
        metricName: 'BlockedRequests',
        dimensionsMap: {
          WebACL: this.webACL.name!,
          Rule: 'RateLimitAPI',
          Region: this.region,
        },
        statistic: 'Sum',
        period: cdk.Duration.minutes(5),
      }),
      threshold: 100,
      evaluationPeriods: 2,
      comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
      treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
    });

    rateLimitAlarm.addAlarmAction(new cloudwatch_actions.SnsAction(topic));

    /**
     * Outputs
     */
    new cdk.CfnOutput(this, 'WebACLArn', {
      value: this.webACL.attrArn,
      description: 'WAF Web ACL ARN',
    });

    new cdk.CfnOutput(this, 'WebACLId', {
      value: this.webACL.attrId,
      description: 'WAF Web ACL ID',
    });
  }
}

CloudFront + WAF 整合

// infrastructure/lib/cloudfront-waf-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
import { WAFStack } from './waf-stack';

export class CloudFrontWAFStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    /**
     * 建立 WAF (必須在 us-east-1 for CloudFront)
     */
    const wafStack = new WAFStack(this, 'CloudFrontWAF', {
      scope: 'CLOUDFRONT',
      env: { region: 'us-east-1' }, // CloudFront WAF 必須在 us-east-1
    });

    /**
     * S3 Bucket for static assets
     */
    const assetsBucket = new s3.Bucket(this, 'AssetsBucket', {
      bucketName: `kyo-assets-${this.account}`,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });

    /**
     * CloudFront Distribution
     */
    const distribution = new cloudfront.Distribution(this, 'KyoDistribution', {
      comment: 'Kyo System CDN',

      // 預設行為:靜態資產
      defaultBehavior: {
        origin: new origins.S3Origin(assetsBucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
        allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
        compress: true,
      },

      // 額外行為:API 請求
      additionalBehaviors: {
        '/api/*': {
          origin: new origins.HttpOrigin('api.kyong.com', {
            protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
            httpsPort: 443,
            customHeaders: {
              'X-Custom-Header': 'kyo-cloudfront',
            },
          }),
          viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
          cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, // API 不快取
          originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER,
          allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
        },
      },

      // 關聯 WAF
      webAclId: wafStack.webACL.attrArn,

      // 自訂錯誤回應
      errorResponses: [
        {
          httpStatus: 403,
          responseHttpStatus: 403,
          responsePagePath: '/error.html',
          ttl: cdk.Duration.seconds(10),
        },
        {
          httpStatus: 404,
          responseHttpStatus: 404,
          responsePagePath: '/404.html',
          ttl: cdk.Duration.seconds(300),
        },
      ],

      // 地理限制(與 WAF 配合)
      geoRestriction: cloudfront.GeoRestriction.allowlist(
        'TW', 'JP', 'US', 'SG'
      ),

      // SSL/TLS 憑證
      certificate: undefined, // 使用 ACM 憑證
      domainNames: ['app.kyong.com', 'www.kyong.com'],

      // 預設根物件
      defaultRootObject: 'index.html',

      // 啟用 IPv6
      enableIpv6: true,

      // 啟用 logging
      enableLogging: true,
      logBucket: new s3.Bucket(this, 'CloudFrontLogsBucket', {
        bucketName: `kyo-cloudfront-logs-${this.account}`,
        encryption: s3.BucketEncryption.S3_MANAGED,
        blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
        lifecycleRules: [
          {
            expiration: cdk.Duration.days(90),
          },
        ],
      }),
      logFilePrefix: 'cloudfront/',
    });

    new cdk.CfnOutput(this, 'DistributionDomainName', {
      value: distribution.distributionDomainName,
      description: 'CloudFront Distribution Domain',
    });
  }
}

Bot Control 與 CAPTCHA 整合

// infrastructure/lib/bot-control-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import { Construct } from 'constructs';

/**
 * AWS WAF Bot Control
 *
 * 功能:
 * - 識別並封鎖惡意機器人
 * - 允許合法爬蟲(如 Google Bot)
 * - CAPTCHA 挑戰可疑流量
 *
 * 定價:
 * - $10/month per Web ACL
 * - $1 per 1M requests
 */
export class BotControlStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 建立 CAPTCHA Token Domain
    // 需要先在 Route53 註冊
    const captchaDomain = 'captcha.kyong.com';

    // 建立包含 Bot Control 的 Web ACL
    const webACL = new wafv2.CfnWebACL(this, 'BotControlWebACL', {
      name: 'kyo-bot-control',
      scope: 'REGIONAL',
      defaultAction: { allow: {} },

      rules: [
        // Bot Control Managed Rule
        {
          name: 'AWSManagedRulesBotControlRuleSet',
          priority: 0,
          statement: {
            managedRuleGroupStatement: {
              vendorName: 'AWS',
              name: 'AWSManagedRulesBotControlRuleSet',
              managedRuleGroupConfigs: [
                {
                  awsManagedRulesBotControlRuleSet: {
                    inspectionLevel: 'COMMON', // COMMON or TARGETED
                  },
                },
              ],
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'BotControl',
          },
        },

        // CAPTCHA for suspicious traffic
        {
          name: 'CAPTCHAForSuspiciousTraffic',
          priority: 1,
          statement: {
            rateBasedStatement: {
              limit: 500, // 5 分鐘內 500 requests
              aggregateKeyType: 'IP',
            },
          },
          action: {
            captcha: {
              customRequestHandling: {
                insertHeaders: [
                  {
                    name: 'x-captcha-required',
                    value: 'true',
                  },
                ],
              },
            },
          },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'CAPTCHAChallenge',
          },
          captchaConfig: {
            immunityTimeProperty: {
              immunityTime: 300, // 通過後 5 分鐘免驗證
            },
          },
        },

        // 允許已知的良好機器人
        {
          name: 'AllowGoodBots',
          priority: 2,
          statement: {
            orStatement: {
              statements: [
                // Google Bot
                {
                  byteMatchStatement: {
                    fieldToMatch: {
                      singleHeader: { name: 'user-agent' },
                    },
                    positionalConstraint: 'CONTAINS',
                    searchString: 'Googlebot',
                    textTransformations: [
                      { priority: 0, type: 'NONE' },
                    ],
                  },
                },
                // Bing Bot
                {
                  byteMatchStatement: {
                    fieldToMatch: {
                      singleHeader: { name: 'user-agent' },
                    },
                    positionalConstraint: 'CONTAINS',
                    searchString: 'bingbot',
                    textTransformations: [
                      { priority: 0, type: 'LOWERCASE' },
                    ],
                  },
                },
              ],
            },
          },
          action: { allow: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'AllowGoodBots',
          },
        },
      ],

      // CAPTCHA 設定
      captchaConfig: {
        immunityTimeProperty: {
          immunityTime: 300, // 預設免疫時間
        },
      },

      // Token Domains (for CAPTCHA)
      tokenDomains: [captchaDomain],

      visibilityConfig: {
        sampledRequestsEnabled: true,
        cloudWatchMetricsEnabled: true,
        metricName: 'BotControlWebACL',
      },
    });

    new cdk.CfnOutput(this, 'CAPTCHADomain', {
      value: captchaDomain,
      description: 'CAPTCHA Token Domain',
    });
  }
}

Lambda@Edge 自動化回應

// infrastructure/lambda-edge/waf-auto-response.ts
/**
 * Lambda@Edge for WAF Auto Response
 *
 * 功能:
 * - 根據威脅等級自動調整 WAF 規則
 * - 動態更新 IP 黑名單
 * - 異常流量自動封鎖
 */

import { CloudFormationCustomResourceEvent } from 'aws-lambda';
import {
  WAFV2Client,
  UpdateIPSetCommand,
  GetIPSetCommand,
} from '@aws-sdk/client-wafv2';

const wafClient = new WAFV2Client({ region: 'us-east-1' });

const BLOCKED_IP_SET_ID = process.env.BLOCKED_IP_SET_ID!;
const BLOCKED_IP_SET_NAME = process.env.BLOCKED_IP_SET_NAME!;

/**
 * CloudWatch Logs 觸發的自動封鎖
 *
 * 場景:
 * 1. 檢測到 DDoS 攻擊
 * 2. 短時間內大量 4xx/5xx
 * 3. SQL Injection 嘗試
 */
export async function handler(event: any) {
  console.log('Event:', JSON.stringify(event, null, 2));

  try {
    // 解析 CloudWatch Logs
    const logEvents = event.awslogs?.data
      ? JSON.parse(
          Buffer.from(event.awslogs.data, 'base64').toString('utf-8')
        ).logEvents
      : [];

    // 分析日誌,找出需要封鎖的 IP
    const suspiciousIPs = new Set<string>();

    for (const logEvent of logEvents) {
      const message = JSON.parse(logEvent.message);

      // 條件 1: Rate Limit 觸發超過 10 次
      if (
        message.action === 'BLOCK' &&
        message.ruleId === 'RateLimitAPI'
      ) {
        suspiciousIPs.add(message.httpRequest.clientIp);
      }

      // 條件 2: SQL Injection 嘗試
      if (
        message.action === 'BLOCK' &&
        message.ruleId?.includes('SQLi')
      ) {
        suspiciousIPs.add(message.httpRequest.clientIp);
      }
    }

    if (suspiciousIPs.size === 0) {
      console.log('No suspicious IPs found');
      return { statusCode: 200, body: 'No action needed' };
    }

    // 取得現有的 IP Set
    const getIPSetResponse = await wafClient.send(
      new GetIPSetCommand({
        Id: BLOCKED_IP_SET_ID,
        Name: BLOCKED_IP_SET_NAME,
        Scope: 'CLOUDFRONT',
      })
    );

    const currentAddresses = getIPSetResponse.IPSet?.Addresses || [];
    const newAddresses = Array.from(suspiciousIPs).map(ip => `${ip}/32`);

    // 合併並去重
    const updatedAddresses = Array.from(
      new Set([...currentAddresses, ...newAddresses])
    );

    // 更新 IP Set
    await wafClient.send(
      new UpdateIPSetCommand({
        Id: BLOCKED_IP_SET_ID,
        Name: BLOCKED_IP_SET_NAME,
        Scope: 'CLOUDFRONT',
        Addresses: updatedAddresses,
        LockToken: getIPSetResponse.LockToken,
      })
    );

    console.log(`Blocked ${suspiciousIPs.size} new IPs:`, Array.from(suspiciousIPs));

    return {
      statusCode: 200,
      body: JSON.stringify({
        blocked: Array.from(suspiciousIPs),
        total: updatedAddresses.length,
      }),
    };
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

日誌分析與威脅偵測

// infrastructure/lib/waf-logging-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as logs from 'aws-cdk-lib/aws-logs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as kinesis from 'aws-cdk-lib/aws-kinesis';
import * as kinesisfirehose from 'aws-cdk-lib/aws-kinesisfirehose';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

/**
 * WAF Logging & Analysis Stack
 *
 * 架構:
 * WAF → Kinesis Data Firehose → S3 → Athena
 */
export class WAFLoggingStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    /**
     * S3 Bucket for WAF logs
     */
    const logBucket = new s3.Bucket(this, 'WAFLogsBucket', {
      bucketName: `kyo-waf-logs-${this.account}`,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      lifecycleRules: [
        {
          transitions: [
            {
              storageClass: s3.StorageClass.INTELLIGENT_TIERING,
              transitionAfter: cdk.Duration.days(30),
            },
            {
              storageClass: s3.StorageClass.GLACIER,
              transitionAfter: cdk.Duration.days(90),
            },
          ],
          expiration: cdk.Duration.days(365),
        },
      ],
    });

    /**
     * Kinesis Data Firehose
     */
    const firehoseRole = new iam.Role(this, 'FirehoseRole', {
      assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'),
    });

    logBucket.grantWrite(firehoseRole);

    const deliveryStream = new kinesisfirehose.CfnDeliveryStream(
      this,
      'WAFLogDeliveryStream',
      {
        deliveryStreamName: 'aws-waf-logs-kyo',
        deliveryStreamType: 'DirectPut',
        s3DestinationConfiguration: {
          bucketArn: logBucket.bucketArn,
          roleArn: firehoseRole.roleArn,
          prefix: 'waf-logs/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/',
          errorOutputPrefix: 'waf-logs-errors/',
          bufferingHints: {
            intervalInSeconds: 300, // 5 分鐘
            sizeInMBs: 5,
          },
          compressionFormat: 'GZIP',
        },
      }
    );

    /**
     * CloudWatch Logs Insights 查詢範例
     */

    // 查詢 1: 最常被封鎖的 IP
    const query1 = `
      fields @timestamp, httpRequest.clientIp, action, terminatingRuleId
      | filter action = "BLOCK"
      | stats count() as blocked_count by httpRequest.clientIp
      | sort blocked_count desc
      | limit 20
    `;

    // 查詢 2: SQL Injection 嘗試
    const query2 = `
      fields @timestamp, httpRequest.clientIp, httpRequest.uri, terminatingRuleId
      | filter terminatingRuleId like /SQLi/
      | sort @timestamp desc
    `;

    // 查詢 3: Rate Limit 觸發統計
    const query3 = `
      fields @timestamp, httpRequest.clientIp, terminatingRuleId
      | filter terminatingRuleId = "RateLimitAPI"
      | stats count() as rate_limit_hits by bin(5m)
    `;

    /**
     * CloudWatch Metric Filters
     */
    const logGroup = logs.LogGroup.fromLogGroupName(
      this,
      'WAFLogGroup',
      'aws-waf-logs-kyo'
    );

    // Metric Filter 1: SQL Injection 嘗試
    new logs.MetricFilter(this, 'SQLInjectionAttempts', {
      logGroup,
      filterPattern: logs.FilterPattern.literal(
        '[..., rule_id=*SQLi*, ...]'
      ),
      metricNamespace: 'Kyo/WAF',
      metricName: 'SQLInjectionAttempts',
      metricValue: '1',
      defaultValue: 0,
    });

    // Metric Filter 2: 高頻封鎖 IP
    new logs.MetricFilter(this, 'HighBlockRate', {
      logGroup,
      filterPattern: logs.FilterPattern.literal('[..., action=BLOCK, ...]'),
      metricNamespace: 'Kyo/WAF',
      metricName: 'BlockedRequests',
      metricValue: '1',
      defaultValue: 0,
    });

    new cdk.CfnOutput(this, 'LogBucketName', {
      value: logBucket.bucketName,
      description: 'WAF Logs S3 Bucket',
    });

    new cdk.CfnOutput(this, 'FirehoseDeliveryStreamName', {
      value: deliveryStream.deliveryStreamName!,
      description: 'Kinesis Firehose Delivery Stream',
    });
  }
}

成本優化策略

/**
 * AWS WAF 成本優化指南
 *
 * 定價結構 (us-east-1):
 * - Web ACL: $5.00/month
 * - Rule: $1.00/month per rule
 * - Request: $0.60 per 1M requests
 * - Bot Control: $10.00/month + $1.00 per 1M requests
 * - CAPTCHA: $0.40 per 1000 challenge attempts
 *
 * 成本估算範例:
 */

interface WAFCostEstimate {
  webACLs: number;
  rulesPerACL: number;
  monthlyRequests: number; // in millions
  botControlEnabled: boolean;
  captchaChallenges: number; // in thousands
}

function calculateWAFCost(config: WAFCostEstimate): {
  webACLCost: number;
  rulesCost: number;
  requestsCost: number;
  botControlCost: number;
  captchaCost: number;
  totalMonthlyCost: number;
} {
  // Web ACL 費用
  const webACLCost = config.webACLs * 5.0;

  // Rules 費用
  const rulesCost = config.webACLs * config.rulesPerACL * 1.0;

  // Requests 費用
  const requestsCost = config.monthlyRequests * 0.6;

  // Bot Control 費用
  const botControlCost = config.botControlEnabled
    ? 10.0 + config.monthlyRequests * 1.0
    : 0;

  // CAPTCHA 費用
  const captchaCost = config.captchaChallenges * 0.4;

  const totalMonthlyCost =
    webACLCost + rulesCost + requestsCost + botControlCost + captchaCost;

  return {
    webACLCost,
    rulesCost,
    requestsCost,
    botControlCost,
    captchaCost,
    totalMonthlyCost,
  };
}

// 範例 1: 小型 SaaS (100M requests/month)
const smallSaaS = calculateWAFCost({
  webACLs: 1,
  rulesPerACL: 10,
  monthlyRequests: 100,
  botControlEnabled: false,
  captchaChallenges: 0,
});

console.log('=== Small SaaS (100M req/month) ===');
console.log('Web ACL: $' + smallSaaS.webACLCost.toFixed(2));
console.log('Rules: $' + smallSaaS.rulesCost.toFixed(2));
console.log('Requests: $' + smallSaaS.requestsCost.toFixed(2));
console.log('Total: $' + smallSaaS.totalMonthlyCost.toFixed(2));

// 範例 2: 中型 SaaS (1B requests/month, Bot Control)
const mediumSaaS = calculateWAFCost({
  webACLs: 2, // CloudFront + ALB
  rulesPerACL: 15,
  monthlyRequests: 1000,
  botControlEnabled: true,
  captchaChallenges: 100,
});

console.log('\n=== Medium SaaS (1B req/month) ===');
console.log('Web ACL: $' + mediumSaaS.webACLCost.toFixed(2));
console.log('Rules: $' + mediumSaaS.rulesCost.toFixed(2));
console.log('Requests: $' + mediumSaaS.requestsCost.toFixed(2));
console.log('Bot Control: $' + mediumSaaS.botControlCost.toFixed(2));
console.log('CAPTCHA: $' + mediumSaaS.captchaCost.toFixed(2));
console.log('Total: $' + mediumSaaS.totalMonthlyCost.toFixed(2));

/**
 * 成本優化策略:
 *
 * 1. 規則優化
 *    ✅ 合併相似規則
 *    ✅ 移除低效規則
 *    ✅ 使用 Managed Rules (包含多個規則但只算 1 個)
 *
 * 2. 流量優化
 *    ✅ CDN 快取減少回源請求
 *    ✅ 靜態資源不經過 WAF
 *    ✅ 健康檢查不計入 WAF
 *
 * 3. Bot Control
 *    ✅ 評估是否真的需要
 *    ✅ 只對關鍵端點啟用
 *    ✅ 使用 Scope Down Statement 縮小範圍
 *
 * 4. CAPTCHA
 *    ✅ 調整觸發閾值
 *    ✅ 延長免疫時間
 *    ✅ 只對高風險操作啟用
 *
 * 5. 監控
 *    ✅ 設定 Cost Anomaly Detection
 *    ✅ 定期檢視 Cost Explorer
 *    ✅ 分析無效規則
 */

今日總結

我們今天完成了 Kyo System 的 AWS WAF 與 Shield 安全防護:

核心功能

  1. WAF Web ACL: 完整的規則配置
  2. Managed Rules: SQL Injection、XSS、Known Bad Inputs
  3. Rate-based Rules: API 速率限制
  4. Geo Blocking: 地理封鎖
  5. Bot Control: 機器人偵測與防護
  6. CAPTCHA: 可疑流量挑戰
  7. 自動化回應: Lambda 動態封鎖
  8. 日誌分析: Kinesis Firehose + Athena

技術比較

WAF vs Application Rate Limiting:

  • WAF: 網路層防護、全域生效
  • Application: 應用層控制、更細緻
  • 💡 兩者互補,雙層防護

Shield Standard vs Advanced:

  • Standard: 免費、L3/L4 DDoS 防護
  • Advanced: $3000/月、DRT 支援、成本保護
  • 💡 一般 SaaS 用 Standard 即可

Bot Control 值得嗎?:

  • 成本: $10 + $1 per 1M requests
  • 效益: 精確識別機器人、減少誤判
  • 💡 高流量網站值得投資

CAPTCHA 最佳實踐:

  • 觸發閾值: 5 分鐘 500 requests
  • 免疫時間: 5-10 分鐘
  • 只對敏感操作啟用
  • 💡 平衡安全性與用戶體驗

WAF 安全檢查清單

  • ✅ Web ACL 建立
  • ✅ Managed Rules 啟用
  • ✅ Rate-based Rules 設定
  • ✅ IP Sets 管理
  • ✅ Geo Blocking 配置
  • ✅ Bot Control 整合
  • ✅ CAPTCHA 挑戰
  • ✅ CloudFront 關聯
  • ✅ ALB 關聯
  • ✅ 日誌記錄
  • ✅ CloudWatch Alarms
  • ✅ 成本監控

上一篇
Day 25: 30天部署SaaS產品到AWS-AWS Cognito 整合與用戶池管理
系列文
30 天將工作室 SaaS 產品部署起來26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言